# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2000, 2003 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307	USA

# Rectangle:
#
# The Rectangle is actually better described as a Parallelogram. When
# created interactively, instances of this class are rectangles with
# edges parallel to the axes of the coordinate system. The user can then
# rotate and shear it interactively so that it may become a
# (nonrectangular) Parallelogram.
#

from math import hypot

from Sketch.const import corners, AlternateMask
from Sketch import _, SingularMatrix, PointsToRect, Trafo, Polar,\
     RoundedRectanglePath, RectanglePath, NullUndo
from Sketch.warn import warn, INTERNAL

import Sketch.UI.skpixmaps
pixmaps = Sketch.UI.skpixmaps.PixmapTk

from base import Primitive, RectangularPrimitive, RectangularCreator, Editor
from bezier import PolyBezier
import handle
from properties import DefaultGraphicsProperties

class Rectangle(RectangularPrimitive):

    is_Rectangle = 1
    is_curve = 1
    is_clip = 1
    has_edit_mode = 1

    _lazy_attrs = RectangularPrimitive._lazy_attrs.copy()
    _lazy_attrs['rect_path'] = 'update_path'

    def __init__(self, trafo = None, radius1 = 0, radius2 = 0,
                 properties = None, duplicate = None):
        RectangularPrimitive.__init__(self, trafo, properties = properties,
                                      duplicate = duplicate)
        if duplicate is not None:
            self.radius1 = duplicate.radius1
            self.radius2 = duplicate.radius2
        else:
            self.radius1 = radius1
            self.radius2 = radius2

    def Radii(self):
        return self.radius1, self.radius2

    def SetTrafoAndRadii(self, trafo, radius1, radius2):
        undo = self.SetTrafoAndRadii, self.trafo, self.radius1, self.radius2
        self.trafo = trafo
        if radius1 <= 0 or radius2 <= 0:
            self.radius1 = 0
            self.radius2 = 0
            if __debug__:
                if radius1 > 0 or radius2 > 0:
                    warn(INTERNAL,
                         'Rectangle radius corrected: r1 = %g, r2 = %g',
                         radius1, radius2)
        else:
            self.radius1 = radius1
            self.radius2 = radius2
        self._changed()
        return undo

    def DrawShape(self, device, rect = None, clip = 0):
        Primitive.DrawShape(self, device)
        if self.radius1 == self.radius2 == 0:
            device.Rectangle(self.trafo, clip)
        else:
            device.RoundedRectangle(self.trafo, self.radius1, self.radius2,
                                    clip)

    def update_path(self):
        if self.radius1 == self.radius2 == 0:
            self.rect_path = (RectanglePath(self.trafo),)
        else:
            self.rect_path = (RoundedRectanglePath(self.trafo, self.radius1,
                                                   self.radius2),)

    def Paths(self):
        return self.rect_path

    def AsBezier(self):
        return PolyBezier(paths = self.rect_path,
                          properties = self.properties.Duplicate())

    def Hit(self, p, rect, device, clip = 0):
        if self.radius1 == self.radius2 == 0:
            return device.ParallelogramHit(p, self.trafo, 1, 1,
                                           clip or self.Filled(),
                                           self.properties,
                                           ignore_outline_mode = clip)
        else:
            return device.MultiBezierHit(self.rect_path, p, self.properties,
                                         clip or self.Filled(),
                                         ignore_outline_mode = clip)
    def GetSnapPoints(self):
        return map(self.trafo, corners + [(0.5, 0.5)])

    def Snap(self, p):
        try:
            x, y = self.trafo.inverse()(p)
            minx = self.radius1
            maxx = 1 - self.radius1
            miny = self.radius2
            maxy = 1 - self.radius2
            if minx < x < maxx:
                if miny < y < maxy:
                    ratio = hypot(self.trafo.m11, self.trafo.m21) \
                          / hypot(self.trafo.m12, self.trafo.m22)
                    if x < 0.5:
                        dx = x
                    else:
                        dx = 1 - x
                    if y < 0.5:
                        dy = y
                    else:
                        dy = 1 - y
                    if dy / dx > ratio:
                        x = round(x)
                    else:
                        y = round(y)
                elif y > maxy:
                    y = 1
                else:
                    y = 0
            elif miny < y < maxy:
                if x > maxx:
                    x = 1
                else:
                    x = 0
            elif minx > 0 and miny > 0:
                # the round corners
                if x < 0.5:
                    cx = minx
                else:
                    cx = maxx
                if y < 0.5:
                    cy = miny
                else:
                    cy = maxy
                trafo = Trafo(minx, 0, 0, miny, cx, cy)
                r, phi = trafo.inverse()(x, y).polar()
                x, y = trafo(Polar(1, phi))
            else:
                # normal corners
                x = round(min(max(x, 0), 1))
                y = round(min(max(y, 0), 1))

            p2 = self.trafo(x, y)
            return (abs(p - p2), p2)
        except SingularMatrix:
            return (1e200, p)

    def update_rects(self):
        rect = PointsToRect(map(self.trafo, corners))
        self.coord_rect = rect
        if self.properties.HasLine():
            self.bounding_rect = rect.grown(self.properties.GrowAmount())
        else:
            self.bounding_rect = rect

    def Info(self):
        trafo = self.trafo
        w = hypot(trafo.m11, trafo.m21)
        h = hypot(trafo.m12, trafo.m22)
        return _("Rectangle %(size)[size]"), {'size': (w, h)}

    def SaveToFile(self, file):
        Primitive.SaveToFile(self, file)
        file.Rectangle(self.trafo, self.radius1, self.radius2)

    def Blend(self, other, p, q):
        result = RectangularPrimitive.Blend(self, other, p, q)
        result.radius1 = p * self.radius1 + q * other.radius1
        result.radius2 = p * self.radius2 + q * other.radius2
        return result

    def Editor(self):
        return RectangleEditor(self)



class RectangleCreator(RectangularCreator):

    creation_text = _("Create Rectangle")

    state = 0

    def MouseMove(self, p, state):
        self.state = state
        RectangularCreator.MouseMove(self, p, state)

    def ButtonUp(self, p, button, state):
        if self.state & AlternateMask:
            p = self.apply_constraint(p, state)
            self.DragStop(p)
            off = 2 * self.off
            end = self.trafo.offset() - self.off
            self.trafo = Trafo(off.x, 0, 0, off.y, end.x, end.y)
        else:
            RectangularCreator.ButtonUp(self, p, button, state)

    def DrawDragged(self, device, partially):
        start = self.drag_start
        if self.state & AlternateMask:
            start = start - self.off
        device.DrawRectangle(start, self.drag_cur)

    def CurrentInfoText(self):
        start = self.drag_start
        if self.state & AlternateMask:
            start = start - self.off
        width, height = self.drag_cur - start
        return 'Rectangle: %(size)[size]', {'size': (abs(width), abs(height))}

    def CreatedObject(self):
        return Rectangle(self.trafo,
                         properties = DefaultGraphicsProperties())


class RectangleEditor(Editor):

    EditedClass = Rectangle

    selection = None

    def ButtonDown(self, p, button, state):
        if self.selection is not None:
            start = self.selection.p
            Editor.DragStart(self, start)
            return p - start
        else:
            return None

    def ButtonUp(self, p, button, state):
        if self.selection is not None:
            trafo, radius1, radius2 = self.resize()
            self.selection = None
            return self.object.SetTrafoAndRadii(trafo, radius1, radius2)
        else:
            return NullUndo

    def resize(self):
        code = self.selection.x_code
        trafo = self.trafo; radius1 = self.radius1; radius2 = self.radius2
        if code < 0:
            # a special handle that has to be treated as a normal handle
            # depending on the direction of the drag
            width = hypot(trafo.m11, trafo.m21)
            height = hypot(trafo.m12, trafo.m22)
            t = Trafo(trafo.m11 / width, trafo.m21 / width,
                      trafo.m12 / height, trafo.m22 / height, 0, 0)
            dx, dy = t.inverse()(self.off)
            #print code, dx, dy
            if code > -5:
                # one of the corners in a rectangle with sharp corners
                if abs(dx) > abs(dy):
                    code = 4 - code
                else:
                    code = (12, 10, 11, 9)[code]
            else:
                # the edge handle and the round corner handles coincide
                if code >= -7:
                    # horizontal edges
                    if abs(dx) > abs(dy):
                        if dx < 0:
                            code = -code
                        else:
                            code = -code + 1
                    else:
                        code = -4 - code
                else:
                    # vertical edges
                    if abs(dx) > abs(dy):
                        code = code + 13
                    else:
                        if dy < 0:
                            code = -code
                        else:
                            code = -code + 1
        #
        # code is now a normal handle
        #
        #print '->', code
        x, y = trafo.inverse()(self.drag_cur)
        width = hypot(trafo.m11, trafo.m21)
        height = hypot(trafo.m12, trafo.m22)
        if code <= 4:
            # drag one of the edges
            if code == 1:
                t = Trafo(1, 0, 0, 1 - y, 0, y)
                if y != 1:
                    radius2 = radius2 / abs(1 - y)
                else:
                    radius1 = radius2 = 0
            elif code == 2:
                t = Trafo(x, 0, 0, 1, 0, 0)
                if x != 0:
                    radius1 = radius1 / abs(x)
                else:
                    radius1 = radius2 = 0
            elif code == 3:
                t = Trafo(1, 0, 0, y, 0, 0)
                if y != 0:
                    radius2 = radius2 / abs(y)
                else:
                    radius1 = radius2 = 0
            elif code == 4:
                t = Trafo(1 - x, 0, 0, 1, x, 0)
                if x != 1:
                    radius1 = radius1 / abs(1 - x)
                else:
                    radius1 = radius2 = 0
            trafo = trafo(t)
            if radius1 != 0 or radius2 != 0:
                ratio = radius1 / radius2
                if radius1 > 0.5:
                    radius1 = 0.5
                    radius2 = radius1 / ratio
                if radius2 > 0.5:
                    radius2 = 0.5
                    radius1 = radius2 * ratio
        else:
            # modify the round corners
            if radius1 == radius2 == 0:
                ratio = height / width
            else:
                ratio = radius1 / radius2

            if ratio > 1:
                max1 = 0.5
                max2 = max1 / ratio
            else:
                max2 = 0.5
                max1 = max2 * ratio
            if code < 9:
                if code == 6 or code == 8:
                    x = 1 - x
                radius1 = max(min(x, max1), 0)
                radius2 = radius1 / ratio
            else:
                if code == 10 or code == 12:
                    y = 1 - y
                radius2 = max(min(y, max2), 0)
                radius1 = radius2 * ratio
        return trafo, radius1, radius2

    def DrawDragged(self, device, partially):
        if self.selection is not None:
            trafo, radius1, radius2 = self.resize()
            device.RoundedRectangle(trafo, radius1, radius2)

    def GetHandles(self):
        trafo = self.trafo; radius1 = self.radius1; radius2 = self.radius2
        handles = []

        if radius1 == radius2 == 0:
            for x, y, code in ((0, 0, -1), (1, 0, -2), (0, 1, -3), (1, 1, -4),
                               (0.5,   0, 1), (1.0, 0.5, 2),
                               (0.5, 1.0, 3), (  0, 0.5, 4)):
                h = handle.MakeNodeHandle(trafo(x, y))
                h.x_code = code
                handles.append(h)
        else:
            # horizontal edges
            if round(radius1, 3) >= 0.5:
                h = handle.MakeNodeHandle(trafo(0.5, 0))
                h.x_code = -5
                handles.append(h)
                h = handle.MakeNodeHandle(trafo(0.5, 1))
                h.x_code = -7
                handles.append(h)
            else:
                coords = ((radius1, 0, 5), (0.5, 0, 1), (1 - radius1, 0, 6),
                          (radius1, 1, 7), (0.5, 1, 3), (1 - radius1, 1, 8))
                for x, y, code in coords:
                    h = handle.MakeNodeHandle(trafo(x, y))
                    h.x_code = code
                    handles.append(h)

            # vertical edges
            if round(radius2, 3) >= 0.5:
                h = handle.MakeNodeHandle(trafo(0, 0.5))
                h.x_code = -9
                handles.append(h)
                h = handle.MakeNodeHandle(trafo(1, 0.5))
                h.x_code = -11
                handles.append(h)
            else:
                coords = ((0, radius2, 9), (0, 0.5, 4), (0, 1 - radius2, 10),
                          (1, radius2, 11),(1, 0.5, 2), (1, 1 - radius2, 12))
                for x, y, code in coords:
                    h = handle.MakeNodeHandle(trafo(x, y))
                    h.x_code = code
                    handles.append(h)
        #
        return handles

    def SelectHandle(self, handle, mode):
        self.selection = handle

    def SelectPoint(self, p, rect, device, mode):
        return 0


